=begin Rquerypad
author: Leon Li(scorpio_leon@hotmail.com)
=end
require 'md5'
class Rquerypad
  #todo scope support
  class << self
    
    def prepare_with_debug
      prepare(true)
    end
    def prepare(debug = $RQUERYPAD_DEBUG)
      return nil unless @prepared.nil?
      @prepared = true
      ActiveRecord::Base.class_eval %{
        require "rquerypad"
        
        class << self
          @@rquerypad_cache = {}
          alias_method(:old_find_4rqp, :find) unless method_defined?(:old_find_4rqp)
          alias_method(:old_cfswia_4rqp, :construct_finder_sql_with_included_associations) unless method_defined?(:old_cfswia_4rqp)
          VALID_FIND_OPTIONS << :inner_joins
          def find(*args)
            options = #{if [].respond_to?(:extract_options!) then "args.extract_options!" else "extract_options_from_args!(args)" end}
            if !options.empty? && (options.include?(:conditions) || options.include?(:group) || options.include?(:order))
            #  if (!options.empty? && (!options[:conditions].nil? || !options[:group].nil? || !options[:order].nil?))
	            #{"p 'original options:', options" if debug}
	            cache_key = Rquerypad.options_key(self, options)
              #p "cache_key: #\{cache_key\}"
              #p "rquerypad_cache[cache_key]: #\{rquerypad_cache[cache_key]\}"
	            if rquerypad_cache[cache_key].nil?
	              #{"p 'process for:', options" if debug}
                Rquerypad.new(self).improve_options!(options)
                rquerypad_cache[cache_key] = Marshal.dump(options)
                #p "rquerypad_cache[cache_key]: #\{rquerypad_cache[cache_key]\}"
              else
                options = Marshal.load(rquerypad_cache[cache_key])
	            end
              #{"p 'new options:', options" if debug}
            end
            args += [options] unless options.empty?
            old_find_4rqp *args 
          end
          def construct_finder_sql_with_included_associations(options, join_dependency)
            sql = old_cfswia_4rqp(options, join_dependency)
            #{"p('original sql', sql) unless options[:inner_joins].nil?" if debug}
            sql = Rquerypad.process_inner_join(sql, options[:inner_joins]) unless options[:inner_joins].nil?
            #{"p('new sql with inner join', sql) unless options[:inner_joins].nil?" if debug}
            sql
          end
          def rquerypad_cache
            @@rquerypad_cache
          end
        end
      }
      
      ActiveRecord::Calculations.class_eval %{
        require "rquerypad"
        module ClassMethods
          @@rquerypad_cache = {}
          alias_method(:old_construct_calculation_sql_4rqp, :construct_calculation_sql) unless method_defined?(:old_construct_calculation_sql_4rqp)
          CALCULATIONS_OPTIONS << :inner_joins
          def construct_calculation_sql(operation, column_name, options)
            if !options.empty? && (options.include?(:conditions) || options.include?(:group) || options.include?(:order))
              #{"p 'original options:', options" if debug}
              cache_key = Rquerypad.options_key(self, options)
              if rquerypad_cache[cache_key].nil?
                #{"p 'process for:', options" if debug}
                Rquerypad.new(self).improve_options!(options)
                rquerypad_cache[cache_key] = Marshal.dump(options)
              else
                options = Marshal.load(rquerypad_cache[cache_key])
              end
              #{"p 'new options:', options" if debug}
            end
            sql = old_construct_calculation_sql_4rqp(operation, column_name, options)
            #{"p('original sql', sql) unless options[:inner_joins].nil?" if debug}
            sql = Rquerypad.process_inner_join(sql, options[:inner_joins]) unless options[:inner_joins].nil?
            #{"p('new sql with inner join', sql) unless options[:inner_joins].nil?" if debug}
            sql
          end
          def rquerypad_cache
            @@rquerypad_cache
          end
        end
      }
      
      Hash.class_eval do
        def single_lt?(other)
          return false if self == other
          return true if self.size == 0
          return false unless other.is_a?(Hash)
          return false if other.nil? || other.size < self.size
          return false if self.size != other.size || self.size > 1 
          a = self.to_a[0]
          o = other.to_a[0]
          return false if a[0] != o[0]
          return true unless a[1].is_a?(Hash)
          a[1].single_lt?(o[1]) 
        end
        def single_gt?(other)
          return false if self == other
          return !(single_lt?(other))
        end
      end
    end
    
    def process_inner_join(sql, inner_joins)
      inner_joins.each {|i| sql.gsub!(/LEFT OUTER JOIN [`]?#{i}[`]?/, "INNER JOIN #{i}")} unless inner_joins.empty?
      sql
    end
    
    def options_key(obj, options)
      key = obj.to_s + options.to_a.to_s
      key = MD5.hexdigest(key) if key.length > 100
      key
    end
  end
  def initialize(obj)
    @owner = obj
    @class_name = obj.to_s.scan(/(\w*::)*([^\(]*)/)[0][1]
    @table_name = @class_name.pluralize.underscore
    @tnwithdot = @table_name + "."
    @new_include = []
    @old_include = nil
    @inner_joins = []
  end
  
  def improve_options!(option_hash)
    
    option_hash.each do |key, value|
      next if value.blank?
      if key.to_s == "conditions"
        option_hash[key] = improve_conditions(value)
      elsif (key.to_s == "order" || key.to_s == "group" || key.to_s == "group_field")
        option_hash[key] = improve_ordergroup(value).join(", ")
      elsif (key.to_s == "include")
        @old_include = value
      end
    end
    return nil if @new_include.empty? 
    #generate new include
    
    remove_dup_includes
    unless @new_include.nil?
      option_hash[:include] = [] if @old_include.nil?
      @new_include += @old_include unless @old_include.nil?
      option_hash.each do |key, value|
        if key.to_s == "include"
          option_hash[key] = @new_include
        end
      end
    end
    option_hash[:inner_joins] = @inner_joins unless @inner_joins.empty? 
  end
  
  def remove_dup_includes
    final_includes = []
    @new_include.each do |ni|
      next if final_includes.include?(ni)
      if final_includes.size > 0
        final_includes.each_index do |i|
          if ni.is_a?(Hash)
            if final_includes[i].is_a?(Hash)
              if final_includes[i].single_lt?(ni)
                final_includes[i] = ni 
              elsif final_includes[i].single_gt?(ni)
              else
                final_includes << ni
              end
            elsif ni.entries[0][0].to_s == final_includes[i].to_s
              final_includes[i] = ni
            else
              final_includes << ni
            end
          else
            if final_includes[i].is_a?(Hash)
              final_includes << ni if final_includes[i].entries[0][0].to_s != ni.to_s
            else
              final_includes << ni if final_includes[i].to_s != ni.to_s
            end
          end
        end
      else
        final_includes << ni
      end
    end
    @new_include = final_includes
  end
  
  def improve_conditions(options)

    if options.is_a?(Hash)
      #work around frozen issue
      new_options = {}
      until options.empty?
        key, value = options.shift
        key = key.to_s.dup
        new_options[process_single!(key)] = value
      end
      options.merge!(new_options)
    else
      if options.is_a?(String)
        options = [options]
      end
      str = options[0]
      qp = /['"][^."]*['"]/
      temp = str.scan(qp)
      #replace string in quote to avoid unecessary processing
      str.gsub!(qp, "'[??]'") unless temp.empty?
      str.gsub!(/(([\w\(\)]+\.)+\w+)[ !><=]/) do |n|
        #cut last char and abstract association for include
        abstract_association(n[0..-2]) + n[-1, 1]
      end
      #add @table_name on single field
      str.gsub!(/[\.:]?\w+[\.]?/) {|x| (x =~ /\D+/).nil? || x[-1, 1] == "." || x[0, 1] == ":" || ["and", "or", "is", "null", "not", "like", "in"].include?(x.downcase) ? x : @tnwithdot + x}
      str.gsub!(/\.#{@table_name}\./, ".")
      #recover string in quote
      unless temp.empty?
        i = -1
        str.gsub!(/\'\[\?\?\]\'/) do
          i += 1
          temp[i]
        end
      end
      options[0] = str
    end
    options
  end
  
  def process_single!(key)
    if key.include?(".")
      if /^(.+)[ ]/ =~ key
        key.sub!("#$1", abstract_association("#$1"))
      else
        key.sub!(/^.+$/, abstract_association(key))
      end
    else
      key.sub!(/^.+$/, @tnwithdot + key)
    end
  end
  
  def improve_ordergroup(fields)
    fields = [fields] if fields.is_a?(String)
    fields.each {|field| process_single!(field)}
    fields
  end
  
  def abstract_association(str)
    result = nil
    owner = @owner
    names = str.split(".")
    return str if names.size == 2 && names[0] == @table_name
    #seperate assocations/tables and field
    tables, field = names[0..-2], names[-1]
    owners = []
    #get relevant owner for each table
    tables.each_index do |i|
      if i == 0
        owners[i] = owner
      else
        tname = cut_end_underscore(tables[i-1])
        r = owners[i-1].reflections[tname.to_sym].options
        owners[i] = r[:class_name].nil? ? eval(owners[i-1].to_s.gsub(/\w*$/, "")+tname.singularize.camelize) : Util::Common.str2class(r[:class_name])
      end
    end
    owners.reverse!
    tables.reverse!
    tables.each_index do |i|
      if tables[i][-1, 1] == "_"
        tables[i].reverse!.sub!("_", "").reverse!
        @inner_joins << transfer_table_name(tables[i], owners[i])
      end
    end
    #process special id field in a belongs_to association
    if field == "id" 
      if owners[0].reflections[tables[0].to_sym].macro.to_s == "belongs_to"
        result = transfer_table_id(tables[0], owners[0])
        @inner_joins.delete(tables[0]) if @inner_joins.include?(tables[0])
        unless owners[1].nil?
          result = transfer_table_name(tables[1], owners[1]) + "." + result
        end
        tables.delete_at(0)
        owners.delete_at(0)
        return result if tables.empty?
      end
    end
    
    #get include
    if tables.length == 1 &&  tables[0] != @table_name
      @new_include << tables[0].to_sym unless @new_include.include?(tables[0].to_sym)
    else
      tables_clone = tables[0..-1]
      value = tables_clone.shift.to_sym
      until tables_clone.empty?
        hashes = {}
        hashes[tables_clone.shift.to_sym] = value
        value = hashes
      end
      @new_include << hashes unless hashes.nil? || @new_include.include?(hashes)
    end
    result ||= transfer_table_name(tables[0], owners[0]) + "." + field
    
  end
  
  def transfer_table_name(name, owner = @owner)
    owner.reflections[name.to_sym].class_name.gsub(/([\w]+::)*/, "").pluralize.underscore
  end
  
  def transfer_table_id(name, owner = @owner)
    owner.reflections[name.to_sym].class_name.gsub(/([\w]+::)*/, "").underscore + "_id"
  end
  
  def cut_end_underscore(str)
    str = str.reverse.sub("_", "").reverse if str[-1, 1] == "_"
    str
  end
  
  def cut_end_underscore!(str)
    str.reverse!.sub!("_", "").reverse! if str[-1, 1] == "_"
  end
end
